class Compile {
  constructor(el, vm) {
    //传入的元素有可能是#app 也有可能是document.querySelector('#app')
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    this.vm = vm();

    if (this.el) {
      //利用文档碎片技巧来获得元素节点
      let docFragment = this.nodeToFragment(this.el);

      //编译
      this.compile(docFragment);

      //编译完后渲染模板
      this.el.appendChild(docFragment);
    }
  }

  /**
   * 判断是否为元素节点
   * @param {*} node 
   * @returns true/false
   */
  isElementNode(node) {
    return node.nodeType === 1;
  }

  /**
   * 利用文档碎片技巧来获得元素节点避免影响性能
   * @param {*} el 
   */
  nodeToFragment(el) {
    //创建文档碎片
    let docFragment = document.createDocumentFragment();

    //循环遍历所有的标签节点
    //停止条件：找到最后一个符合条件即停止 firshChild = null
    // el.firstChild => 该元素的第一个子节点
    let firstChild;
    while (firstChild = el.firstChild) {
      /**
       // console.log(firstChild);
       * #text 文本节点
       * <input type="text"> 元素节点
       * #text 文本节点
       * <div>{{message}}</div> 元素节点
       * #text 文本节点
       */
      //逐一保存到文档碎片docFragment里面
      docFragment.appendChild(firstChild);
    };
    return docFragment;
  }

  /**
   * 编译
   * @param {*} docFragment 文档碎片
   */
  compile(docFragment) {
    let childNodes = docFragment.childNodes;
    // console.log(childNodes);
    //NodeList(5) [text, input, text, div, text]

    //将类数组转为数组并循环得到每一个节点
    Array.from(childNodes).forEach(node => {
      if (this.isElementNode(node)) {
        //元素节点

        //遍历元素节点
        this.compileElement(node);

        //深度遍历嵌套的元素标签<ul><li></li></ul>
        this.compile(node);
      } else {
        //文本节点

        //遍历文本节点
        this.compileText(node);
      }
    });
  }

  /**
   * 遍历元素节点
   * @param {*} node 
   */
  compileElement(node) {
    //拿到v-model属性
    let attrs = node.attributes;
    // console.log(attrs);
    //NamedNodeMap {0: type, 1: v-model, type: type, v-model: v-model, length: 2}

    //将类数组转为数组并循环得到每一个属性
    Array.from(attrs).forEach(attr => {
      // console.log(attr);
      //type = "text"
      //v-model = "message"

      // console.log(attr.name);
      //type
      //v-model

      // console.log(attr.value);
      //text
      //message

      let attrName = attr.name;
      if (this.isDirective(attrName)) {
        let value = attr.value;
        // console.log(value);
        //message

        //拿到 v- 后面的字符
        let type = attrName.slice(2);
        // console.log(type);
        //model

        //attr.value找到的值相当于this.vm.$data对应的值
        //拿到CompileUtils对象下的model方法并传入参数
        //参数：节点/实例/表达式
        CompileUtils[type](node, this.vm, value);
      }
    })
  }

  /**
   * 遍历文本节点
   * 例如：{{message}}
   * @param {*} node 
   */
  compileText(node) {
    let exp = node.textContent;
    // console.log(exp);
    //{{message}}

    //正则匹配{{}}
    //问题：{{abcde}} 里面不能有{符号在 {{ab{cde}}里 否则匹配内容错误
    //解决：加[] 非} 的情况，一个或者多个
    let reg = /\{\{([^}]+)\}\}/g;

    //如果能匹配
    if (reg.test(exp)) {
      //拿到CompileUtils对象下的text方法并传入参数
      CompileUtils['text'](node, this.vm, exp);
    }

  }

  /**
   * 判断是否是v-开头的指令
   * @param {*} attrName 
   * @returns true/false
   */
  isDirective(attrName) {
    return attrName.includes('v-');
  }


}

//编译工具
CompileUtils = {
  /**
   * 
   * @param {*} node 节点
   * @param {*} vm 实例
   * @param {*} exp 表达式
   */
  model(node, vm, exp) {
    //调用updater对象里面的modelUpdater方法
    let updateFn = this.updater['modelUpdater'];

    //实例化数据侦听
    new Watcher(vm, exp, () => {
      //判断是否有该函数，有的情况下才执行
      //data里数据有可能存在嵌套的情况，用getVal方法拿到嵌套里面的值
      //data(){return{message: {a: xxxx}}}
      //拿到vm.$data[表达式]
      //链式调用执行写法 vm.$data.message/vm.$data.color.red
      updateFn && updateFn(node, this.getVal(vm, exp));
    });

    node.addEventListener('input', (e) => {
      let newValue = e.target.value;
      this.setValue(vm, exp, newValue);
    })

    updateFn && updateFn(node, this.getVal(vm, exp));
  },
  text(node, vm, exp) {
    /**
     * console.log(vm);
     * {
     *   message: "Hello World", 
     *   color: color: {red: "#123334", orange: "#333344"}
     * }
     */

    let updateFn = this.updater['textUpdater'];
    //去掉{{}}
    let reg = /\{\{([^}]+)\}\}/g;
    let value = this.getTextValue(vm, exp);

    exp.replace(reg, (...args) => {
      //args[1] => color.orange
      //this.getVal(vm, args[1]) => #333344
      //实例化数据侦听
      new Watcher(vm, args[1], () => {
        updateFn && updateFn(node, this.getTextValue(vm, exp));
      });
    });
    updateFn && updateFn(node, value);
  },
  //实现数据双向绑定更新的方法
  updater: {
    //更新input的输入框内容
    modelUpdater(node, value) {
      // console.log(node);
      //<input type="text" v-model="message">
      // console.log(value);
      //Hello World

      node.value = value;
    },
    //更新文本内容
    textUpdater(node, value) {
      // console.log(node);
      //<div>{{message}}</div>

      // console.log(value);
      //#333344
      //Hello World

      node.textContent = value;
    }
  },
  /**
   * 用getVal方法拿到data对象数据嵌套里面的值
   * message / message.a
   * @param {*} vm 实例
   * @param {*} exp 表达式
   * @return 返回data对象数据嵌套里面的值
   */
  getVal(vm, exp) {
    // console.log(exp);
    //message

    //message.a.b.c => [message, a, b, c]
    exp = exp.split('.');
    // console.log(exp);
    //["message"]
    //["color", "red"]

    //归纳解决多层对象数据嵌套的问题
    //累加器prev/vm.$data
    //当前项next

    /**
     * console.log(vm);
     * {
     *   message: "Hello World", 
     *   color: color: {red: "#123334", orange: "#333344"}
     * }
     */

    return exp.reduce((prev, next) => {
      // console.log(next); message/color/red
      // console.log(prev[next]); 
      // Hello World / { red: "#123334", orange: "#333344" } /#123334
      return prev[next];
    }, vm);
  },
  /**
   * 提取{{}}内容
   * @param {*} vm 
   * @param {*} exp 
   */
  getTextValue(vm, exp) {
    //去掉{{}}
    let reg = /\{\{([^}]+)\}\}/g;
    let value = exp.replace(reg, (...args) => {
      //args[1] => color.orange
      //this.getVal(vm, args[1]) => #333344
      return this.getVal(vm, args[1]);
    });
    return value;
    // console.log(value);
    //#333344
    //Hello World
  },
  setValue(vm, exp, newValue) {
    exp = exp.split('.');
    return exp.reduce((prev, next, currentIndex) => {
      if (currentIndex === exp.length - 1) {
        return prev[next] = newValue;
      }
      return prev[next];
    }, vm);
  }
}